/*******************************************************************************************
 * Copyright (c) 2006-2009 Alex Robson, Rob Simmons, Craig Israel, and Rachel Twyford
 *
 * Use of this software in any form requires that you have both 
 * read and agreed to the following terms:
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the "Software"), 
 * to deal in the Software without restriction, including without limitation 
 * the rights to use, copy, modify, merge, publish, distribute, sublicense, 
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions: 
 *
 * Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 * Redistributions in binary form must reproduce the above copyright notice, 
 * this list of conditions and the following disclaimer in the 
 * documentation and/or other materials provided with the distribution.
 * Neither the name of Nvigorate nor the names of its contributors may 
 * be used to endorse or promote products derived from this software without 
 * specific prior written permission. 
 *
 * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
 * DISCLAIMED. IN NO EVENT SHALL ANY CONTRIBUTOR BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 * ****************************************************************************************/

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Common;
using System.Linq;
using Nvigorate.Common;
using Nvigorate.Logging;

namespace Nvigorate.Data
{
    internal class DatabaseCommand : IDatabaseCommand
    {
        protected IDbCommand _command;
        protected IDatabaseProvider _provider;
        protected ITransactionManager _transactionManager;

        public IDbDataParameter this[string parameterName]
        {
            get { return (IDbDataParameter) _command.Parameters[parameterName]; }
        }

        #region Transaction Management

        public void AbortTransaction()
        {
            _transactionManager.AbortTransaction();
            var disposableScope = _transactionManager as IDisposable;
            if (disposableScope != null) disposableScope.Dispose();
        }

        /// <summary>
        /// Starts a new transaction which all future commands which will be executed under until
        /// an Abort or CompleteTransaction is called.
        /// </summary>
        public virtual void BeginTransaction()
        {
            _transactionManager.BeginTransaction();
        }

        public virtual void CompleteTransaction()
        {
            _transactionManager.CompleteTransaction();
            var disposableScope = _transactionManager as IDisposable;
            if (disposableScope != null) disposableScope.Dispose();
        }

        #endregion

        public DataTable BuildTable(IDataReader reader, string tableName)
        {
            DataTable table = new DataTable(tableName);
            try
            {
                table.BeginLoadData();
                table.Load(reader);
                table.EndLoadData();
            }
            catch (Exception ex)
            {
                DataInterfaceException exception = GetException(ex);
                LogManager.LogError(exception, Severities.Critical);
                throw exception;
            }
            finally
            {
                reader.Close();
            }
            return table;
        }

        public DataSet BuildSet(IDataReader reader, string datasetName, string[] tables)
        {
            DataSet dataset = new DataSet(datasetName);
            try
            {
                dataset.Load(reader, LoadOption.PreserveChanges, tables);
            }
            catch (Exception ex)
            {
                DataInterfaceException exception = GetException(ex);
                LogManager.LogError(exception, Severities.Critical);
                throw exception;
            }
            finally
            {
                reader.Close();
            }
            return dataset;
        }

        public virtual DataSet GetDataSet(IDbConnection connection, string datasetName, string[] tables)
        {
            using (IDataReader reader = ExecuteCommand(connection, false, (c) => c.ExecuteReader()))
            {
                var dataSet = BuildSet(reader, datasetName, tables);
                connection.Close();
                return dataSet;
            }
        }

        public virtual IEnumerable<T> ProcessReader<T>(IDbConnection connection, Func<IDataReader, IEnumerable<T>> process)
        {
            using (IDataReader reader = ExecuteCommand(connection, false, (c) => c.ExecuteReader()))
            {
                return process(reader);
            }
        }

        public virtual DataTable GetDataTable(IDbConnection connection, string tableName)
        {
            using (IDataReader reader = ExecuteCommand(connection, false, (c) => c.ExecuteReader()))
            {
                var dataTable = BuildTable(reader, tableName);
                connection.Close();
                return dataTable;
            }
        }

        public virtual object GetScalar(IDbConnection connection)
        {
            var scalar = ExecuteCommand(connection, false, (c) => c.ExecuteScalar());
            connection.Close();
            return scalar;
        }

        public virtual int Execute(IDbConnection connection, bool useTransactionScope)
        {
            var affected = ExecuteCommand(connection, useTransactionScope, (c) => c.ExecuteNonQuery());
            connection.Close();
            return affected;
        }

        public virtual DataTable ExecuteWithTableResult(IDbConnection connection, bool useTransactionScope, string tableName)
        {
            using (IDataReader reader = ExecuteCommand(connection, useTransactionScope, (c) => c.ExecuteReader()))
            {
                var dataTable = BuildTable(reader, tableName);
                if(useTransactionScope)
                    CompleteTransaction();
                connection.Close();
                return dataTable;
            }
        }

        public virtual DataSet ExecuteWithSetResult(IDbConnection connection, bool useTransactionScope, string datasetName, string[] tables)
        {
            using (IDataReader reader = ExecuteCommand(connection, useTransactionScope, (c) => c.ExecuteReader()))
            {
                var dataSet = BuildSet(reader, datasetName, tables);
                if (useTransactionScope)
                    CompleteTransaction();
                connection.Close();
                return dataSet;
            }
        }

        public virtual T ExecuteCommand<T>(IDbConnection connection, bool useTransactionScope,
                                           Func<IDbCommand, T> commandWrapper)
        {
            if (useTransactionScope)
                BeginTransaction();
            try
            {
                _command.Connection = connection;
                
                if (connection.State != ConnectionState.Open)
                    connection.Open();
                _command.Prepare();
                T result = commandWrapper(_command);

                if (useTransactionScope && !(typeof(T).Equals(typeof(IDataReader))))
                    CompleteTransaction();
                return result;
            }
            catch(DbException dbex)
            {
                if(useTransactionScope)
                    AbortTransaction();

                if (_provider.GetErrorCodeFromDbException(dbex) == 14)
                    throw new ConcurrencyException().FailedToUpdate(dbex.Message);

                DataInterfaceException exception = GetException(dbex);
                throw exception;
            }
            catch (Exception ex)
            {
                DataInterfaceException exception = GetException(ex);
                if (useTransactionScope)
                    AbortTransaction();
                connection.Close();
                throw exception;
            }
        }

        public DataInterfaceException GetException(Exception ex)
        {
            string parameterList = "No parameters provided.";

            if (_command.Parameters.Count > 0)
            {
                DelimitedBuilder paramListBuilder = new DelimitedBuilder(",");
                foreach (IDbDataParameter p in _command.Parameters)
                    paramListBuilder.AppendFormat("{0} = [{1}]", p.ParameterName,
                                                  p.Value == DBNull.Value ? "DBNull" : p.Value);
                parameterList = paramListBuilder.ToString();
            }
            return new DataInterfaceException(ex,
                                              string.Format(
                                                  "An error occurred trying to execute the command {0}. Parameters supplied: {1}",
                                                  _command.CommandText,
                                                  parameterList)
                );
        }

        public virtual void SetParameters(IEnumerable<IDbDataParameter> parameters)
        {
            _command.Parameters.Clear();
            AddParameters(parameters);
        }

        public virtual void SetParameters(Dictionary<string, object> parameters)
        {
            _command.Parameters.Clear();

            foreach (var pair in parameters)
            {
                _command.Parameters.Add(_provider.GetParameter(pair.Key, pair.Value, ParameterDirection.InputOutput));
            }
        }

        public virtual void SetParameters(object[] values)
        {
            if (values == null || values.Length == 0) return;

            //Check to make sure parameters exist before we try setting values
            if (_command.Parameters.Count == 0)
                throw new DataInterfaceException(
                    "You cannot supply an object[] to populate parameters unless you provide parameters during initialization.");

            //Check to ensure the correct number of parameters were supplied
            if (_command.Parameters.Count != values.Length)
                throw new DataInterfaceException(
                    string.Format(
                        "Parameters supplied ({0}) for call {1} do not match the number of registered parameters for this command ({2})",
                        values.Length,
                        _command.CommandText,
                        _command.Parameters.Count
                        ));

            int index = 0;
            foreach (IDbDataParameter parameter in _command.Parameters)
            {
                parameter.Value = values[index];
                index++;
            }
        }

        #region Constructors

        internal DatabaseCommand(string command, IDatabaseProvider provider) : this (
            provider == null ? null : provider.GetCommand(command), provider)
        {
        }

        internal DatabaseCommand(IDbCommand command, IDatabaseProvider provider) : this(command, provider,
            new IDbDataParameter[]{})
        {
        }

        internal DatabaseCommand(string command, IDatabaseProvider provider,
                                 IEnumerable<IDbDataParameter> parameters) : this(provider == null ? null : provider.GetCommand(command), provider, parameters)
        {
        }

        internal DatabaseCommand(IDbCommand command, IDatabaseProvider provider,
                                 IEnumerable<IDbDataParameter> parameters) : this(command, provider,
            new TransactionManager(), parameters)
        {
            
        }

        internal DatabaseCommand(IDbCommand command, 
            IDatabaseProvider provider, ITransactionManager transactionManager,
            IEnumerable<IDbDataParameter> parameters)
        {
            if (command == null) throw new ArgumentNullException("command");
            if (provider == null) throw new ArgumentNullException("provider");
            if (transactionManager == null) throw new ArgumentNullException("transactionManager");

            _command = command;
            _provider = provider;
            _transactionManager = transactionManager;
            AddParameters(parameters);
        }

        #endregion

        #region Private Methods

        private void AddParameters(IEnumerable<IDbDataParameter> parameters)
        {
            if (parameters != null && parameters.Count() > 0)
                parameters
                    .ToList()
                    .ForEach(p => _command.Parameters.Add(p));
        }

        #endregion
    }
}